/*
 * Copyright (C) 2010 Eric Bottard
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.excilys.oss.dao.dynamicfinders;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.CallbackFilter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.NoOp;

/**
 * Base class for providing implementations of DAOs from an interface alone.
 * Subclasses should implement {@link #buildInterceptor(Class, Map)} and may use
 * ORM specific concepts (such as an Hibernate SessionFactory) if they wish.
 * Implementations should be thread safe, typically stateless.
 * 
 * @author Eric Bottard
 * 
 */
public abstract class DynamicFinderFactory {

	private static final String PROPERTY = "[A-Z][a-zA-Z]*?";

	private static final String BOOL_OP = "And|Or";

	private static final String ALL_COMPARATORS;
	static {
		StringBuilder sb = new StringBuilder();
		for (Comparator c : Comparator.values()) {
			sb.append(c.toString()).append('|');
		}
		ALL_COMPARATORS = sb.substring(0, sb.length() - 1);
	}

	private static final String ONE_PROP_FINDER = "(" + PROPERTY + ")("
			+ ALL_COMPARATORS + ")?";

	private static final Pattern METHOD_PATTERN = Pattern
			.compile("^find(All)?By" + ONE_PROP_FINDER + "((" + BOOL_OP + ")"
					+ ONE_PROP_FINDER + ")?$");

	public static void main(String[] args) {
		System.out.println(METHOD_PATTERN);
	}

	@SuppressWarnings("unchecked")
	public <T> T buildDAO(Class<T> contract) {
		DataAccessObject daoAnno = checkDAOAnnotation(contract);
		Class<?> superClass = daoAnno.stub();
		if (superClass == Object.class) {
			// No separate stub.
			// Use the "contract" class as superclass (cglib will revert to
			// interface
			// mode if contract is actually an interface)
			superClass = contract;
		}
		Class[] interfaces = contract.isInterface() ? new Class[] { contract }
				: new Class[0];

		final Set<Method> candidateMethods = selectCandidateMethods(superClass);

		Map<Method, FinderDescriptor> descriptors = buildFinderDescriptors(
				contract, candidateMethods);
		Callback interceptor = buildInterceptor(contract, descriptors);

		// Either intercept the method or go straight to the super
		// implementation
		Callback[] callbacks = new Callback[] { interceptor, NoOp.INSTANCE };
		CallbackFilter abstractMethodFilter = new CallbackFilter() {
			public int accept(Method method) {
				return candidateMethods.contains(method) ? 0 : 1;
			}
		};

		return (T) Enhancer.create(superClass, interfaces,
				abstractMethodFilter, callbacks);
	}

	private <T> DataAccessObject checkDAOAnnotation(Class<T> itf) {
		DataAccessObject daoAnno = itf.getAnnotation(DataAccessObject.class);
		if (daoAnno == null) {
			throw new IllegalArgumentException(
					"Expected an interface (or abstract class) that bears the "
							+ DataAccessObject.class.getSimpleName()
							+ " annotation : " + itf);
		} else if (daoAnno.entityType() == Void.class) {
			throw new IllegalArgumentException("Missing entityType() for "
					+ itf);
		}
		return daoAnno;
	}

	protected abstract <T> Callback buildInterceptor(Class<T> itf,
			Map<Method, FinderDescriptor> descriptors);

	/**
	 * Only keep abstract methods.
	 */
	private Set<Method> selectCandidateMethods(Class<?> itf) {
		Set<Method> ret = new java.util.HashSet<Method>();
		for (Method m : itf.getMethods()) {
			if ((m.getModifiers() & Modifier.ABSTRACT) == Modifier.ABSTRACT) {
				ret.add(m);
			}
		}
		return ret;
	}

	private <T> Map<Method, FinderDescriptor> buildFinderDescriptors(
			Class<T> itf, Set<Method> methods) {

		Map<Method, FinderDescriptor> descriptors = new HashMap<Method, FinderDescriptor>(
				methods.size());

		for (Method m : methods) {
			Matcher matcher = METHOD_PATTERN.matcher(m.getName());
			if (!matcher.matches()) {
				throw new IllegalArgumentException("Unable to parse " + m
						+ " as a finder method");
			}
			String all = matcher.group(1);
			if (all == null
					&& Collection.class.isAssignableFrom(m.getReturnType())) {
				throw new IllegalArgumentException(
						"Incompatible return type for " + m
								+ ". Expected a single result.");
			} else if (all != null
					&& !List.class.isAssignableFrom(m.getReturnType())) {
				throw new IllegalArgumentException(
						"Incompatible return type for " + m
								+ ". Expected a java.util.List");
			}

			int neededArgs = 0;

			String propertyName1 = matcher.group(2);
			propertyName1 = propertyName1.substring(0, 1).toLowerCase()
					+ propertyName1.substring(1);

			String comparatorString1 = matcher.group(3);
			Comparator comp1 = (comparatorString1 == null) ? Comparator.Equals
					: Comparator.valueOf(comparatorString1);

			neededArgs += comp1.nbArguments;

			String boolComb = null;
			String propertyName2 = null;
			Comparator comp2 = null;

			if (null != matcher.group(4)) {
				boolComb = matcher.group(5);
				propertyName2 = matcher.group(6);
				propertyName2 = propertyName2.substring(0, 1).toLowerCase()
						+ propertyName2.substring(1);
				String comparatorString2 = matcher.group(7);
				comp2 = (comparatorString2 == null) ? Comparator.Equals
						: Comparator.valueOf(comparatorString2);
				neededArgs += comp2.nbArguments;
			}

			if (neededArgs != m.getParameterTypes().length) {
				throw new IllegalArgumentException(m
						+ " has the wrong number of arguments. " + neededArgs
						+ " expected.");
			}

			BooleanCombinator combinator = boolComb != null ? BooleanCombinator
					.valueOf(boolComb) : null;
			descriptors.put(m, new FinderDescriptor(all != null, combinator,
					propertyName1, comp1, propertyName2, comp2));

		}

		if (descriptors.size() == 0) {
			throw new IllegalArgumentException("Type " + itf
					+ " has no finder methods. At least one finder is expected");
		}

		return descriptors;
	}
}
